﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using SharpAnalyzer.Collectors;
using SharpAnalyzer.Entities;
using SharpAnalyzer.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SharpAnalyzer.Analyzers
{
    internal class LackOfCohesionAnalyzer : CSharpSyntaxWalker
    {
        private readonly IDictionary<string, int> numFieldAccesses = new Dictionary<string, int>();
        private readonly SemanticModel semanticModel;
        private int numFields;
        private int numInstanceFields;
        private int numMethods;
        private INamedTypeSymbol typeSymbol;

        public LackOfCohesionAnalyzer(SemanticModel model)
          : base(SyntaxWalkerDepth.Node)
        {
            this.semanticModel = model;
        }

        // http://eclipse-metrics.sourceforge.net/descriptions/pages/cohesion/LackOfCohesionInMethods.html
        public LcomMetric Calculate(TypeDeclarationSyntax type)
        {
            typeSymbol = semanticModel.GetDeclaredSymbol(type);

            // Get all Type members
            IEnumerable<SyntaxNode> memberNodes = new SimpleMemberCollectorSyntaxWalker().GetMembers(semanticModel, type);
            
            // calculate [mA, m , a]
            FilterInstanceFieldDeclarations(memberNodes.OfType<FieldDeclarationSyntax>());
            FilterInstanceAutoProperties(memberNodes.OfType<PropertyDeclarationSyntax>());
            Visit(type);

            //numFieldAccesses: number of methods that access a variable(attribute)
            double sumMA = numFieldAccesses.Values.Sum();

            // lcom2 = 1 - sum(mA)/(m*a)
            double lcom2 = Math.Round(1.0 - sumMA / (double)checked(numMethods * numInstanceFields), 2);
            if (double.IsNaN(lcom2) || lcom2 < 0.0)
                lcom2 = 0.0;

            // lcom3 = (m - sum(mA) / a) / (m - 1)
            double lcom3 = Math.Round(((double)numMethods - sumMA / (double)numInstanceFields) / (double)checked(numMethods - 1), 2);
            if (double.IsNaN(lcom3) || lcom3 < 0.0)
                lcom3 = 0.0;
            return new LcomMetric()
            {
                Lcom = lcom2,
                LcomHendersonSellers = lcom3,
                NumFields = numFields,
                NumMethods = numMethods
            };
        }

        public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
        {
            checked { ++numMethods; }
            FilterFieldAccessExpressions(node);
            base.VisitConstructorDeclaration(node);
        }

        public override void VisitDestructorDeclaration(DestructorDeclarationSyntax node)
        {
            checked { ++numMethods; }
            FilterFieldAccessExpressions(node);
            base.VisitDestructorDeclaration(node);
        }

        public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
        {
            checked { ++numMethods; }
            FilterFieldAccessExpressions(node);
            base.VisitMethodDeclaration(node);
        }

        public override void VisitEventDeclaration(EventDeclarationSyntax node)
        {
            FilterAccessors(node.AccessorList, SyntaxKind.AddAccessorDeclaration, SyntaxKind.RemoveAccessorDeclaration);
            base.VisitEventDeclaration(node);
        }

        public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node)
        {
            FilterAccessors(node.AccessorList, SyntaxKind.GetAccessorDeclaration, SyntaxKind.SetAccessorDeclaration);
            base.VisitPropertyDeclaration(node);
        }

        private void FilterAccessors(AccessorListSyntax accessorList, params SyntaxKind[] accessorKinds)
        {
            SyntaxList<AccessorDeclarationSyntax> accessors = accessorList.Accessors;
            foreach (SyntaxKind syntaxKind in accessorKinds)
            {
                SyntaxKind kind = syntaxKind;
                AccessorDeclarationSyntax declarationSyntax = accessors.SingleOrDefault(syntax => syntax.Kind() == kind);
                if (declarationSyntax != null && declarationSyntax.Body != null)
                {
                    checked { ++numMethods; }
                    FilterFieldAccessExpressions(declarationSyntax.Body);
                }
            }
        }

        private void FilterFieldAccessExpressions(SyntaxNode node)
        {
            if (node == null)
                return;
            var allMemberAccesses = node
                .DescendantNodes() // (Func<SyntaxNode, bool>)null, false
                .OfType<MemberAccessExpressionSyntax>();
            var source = FilterFieldAccessExpressions(allMemberAccesses);

            foreach (string str in source.Distinct())
            {
                IDictionary<string, int> dictionary;
                string index;
                (dictionary = numFieldAccesses)[index = str] = checked(dictionary[index] + 1);
            }
        }

        private void FilterInstanceFieldDeclarations(IEnumerable<FieldDeclarationSyntax> nodes)
        {
            foreach (FieldDeclarationSyntax node in nodes)
            {
                checked { ++numFields; }
                if (!node.Modifiers.Any(x => x.ValueText == "static"))
                {
                    checked { ++numInstanceFields; }
                    ISymbol declaredSymbol =
                        FieldDeclarationSyntaxExtensions.GetDeclaredSymbol(node, semanticModel);
                    if (declaredSymbol != null)
                        numFieldAccesses[declaredSymbol.Name] = 0; // for now there is no access to this field
                }
            }
        }

        private void FilterInstanceAutoProperties(IEnumerable<PropertyDeclarationSyntax> nodes)
        {
            foreach (PropertyDeclarationSyntax declarationSyntax1 in nodes)
            {
                if (!declarationSyntax1.Modifiers.Any(x => x.ValueText == "static"))
                {
                    SyntaxList<AccessorDeclarationSyntax> accessors = declarationSyntax1.AccessorList.Accessors;
                    AccessorDeclarationSyntax getter = accessors
                        .SingleOrDefault(syntax => syntax.Kind() == SyntaxKind.GetAccessorDeclaration);
                    AccessorDeclarationSyntax setter = accessors
                        .SingleOrDefault(syntax => syntax.Kind() == SyntaxKind.SetAccessorDeclaration);
                    if ((getter != null || setter != null) && // it has a getter or a setter
                        ((getter == null || getter.Body == null) && // AND no getter body
                         (setter == null || setter.Body == null))) // AND no setter body  >>>>> AUTO Property
                    {
                        checked { ++numFields; }
                        ISymbol symbol = semanticModel.GetDeclaredSymbol(declarationSyntax1);
                        if (symbol != null)
                            numFieldAccesses[symbol.Name] = 0; // for now there is no access to this property
                    }
                }
            }
        }

        private IEnumerable<string> FilterFieldAccessExpressions(IEnumerable<MemberAccessExpressionSyntax> memberAccessExpressions)
        {
            foreach (MemberAccessExpressionSyntax expressionSyntax in memberAccessExpressions)
            {
                SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(expressionSyntax);
                ISymbol symbol = symbolInfo.Symbol;
                if (symbol != null)
                {
                    ISymbol containingSymbol = symbol.ContainingSymbol;
                    if (typeSymbol.Equals((object)containingSymbol))
                    {
                        switch (symbol.Kind)
                        {
                            case SymbolKind.Field:
                            case SymbolKind.Property:
                                string name = symbol.Name;
                                if (numFieldAccesses.ContainsKey(name))
                                {
                                    yield return name;
                                    continue;
                                }
                                continue;
                            default:
                                continue;
                        }
                    }
                }
            }
        }
    }
}
